package jcolorlinez;

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class Grid {
	private int[][] model;
	private int maxValue;
	private String saveFile;
	private PathFinder finder;
	public Grid(int rows, int cols, int maxValue, String saveFile, PathFinder finder) {
		this.model = new int[rows][cols];
		this.maxValue= maxValue;
		this.saveFile = saveFile;
		this.finder = finder;
		this.animationThread.start();
	}
	public int getRows() {
		return model.length;
	}
	public int getColumns() {
		return model[0].length;
	}
	public int get(int row, int col) {
		if (row == ROW_FOR_NEW_CHESS) {
			return nextRoundValues[col];
		} else {
			return model[row][col];
		}
	}
	public int get(Cell cell) {
		return get(cell.row, cell.col);
	}
	public void set(int row, int col, int value) {
		model[row][col] = value;
		this.alert(row, col);
	}
	public void set(Cell cell, int value) {
		set(cell.row, cell.col, value);
	}
	public boolean contains(Cell cell) {
		return (cell.row>=0 && cell.row<getRows() && cell.col>=0 && cell.col<getColumns());
	}
	public int[][] toMatrix() {
		int[][] matrix = new int[getRows()][getColumns()];
		for (int row=0; row<getRows(); row++) {
			System.arraycopy(model[row], 0, matrix[row], 0, getColumns());
		}
		return matrix;
	}
	public void init() {
		for (int row = 0; row < getRows(); row++) {
			for (int col = 0; col < getColumns(); col++) {
				set(row, col, 0);
			}
		}
		generateRandomValues();
		placeRandomValues();  //generate 3 chess
		placeRandomValues(2); //generate 2 chess, together to get 5 chess...
		addScore(-score); //reset score to zero
	}
	public boolean save() {
		try (PrintWriter out = new PrintWriter(new FileWriter(saveFile));) {
			//record, randomValues for next round, score
			out.print(record + " ");
			for (int ran : nextRoundValues) {
				out.print(ran + " ");
			}
			out.println(score);
			
			//chess on board
			for (int row = 0; row < getRows(); row++) {
				for (int col = 0; col < getColumns(); col++) {
					out.print(get(row, col) + " ");
				}
				out.println();
			}
			return true;
		} catch (IOException e) {
			e.printStackTrace();
			return false;
		}
	}
	public boolean load() {
		File file = new File(saveFile);
		if (! file.exists()) {
			return false;
		}
		try (FileReader in = new FileReader(file);) {
			char[] cbuf = new char[(int)file.length()];
			in.read(cbuf);
			String[] values = new String(cbuf).split("[ \r\n\t]+");
			if (values.length != 1 + NEW_CHESS_PER_ROUND + 1 + getRows()*getColumns()) {
				return false;
			}
			int index = 0;
			record = Integer.parseInt(values[index++]);
			for (int i = 0; i<NEW_CHESS_PER_ROUND; i++) {
				nextRoundValues[i] = Integer.parseInt(values[index++]);
			}
			score = Integer.parseInt(values[index++]);
			
			for (int row=0; row<getRows(); row++) {
				for (int col=0; col<getColumns(); col++) {
					set(row, col, Integer.parseInt(values[index++]));
				}
			}
			return true;
		} catch (IOException e) {
			e.printStackTrace();
			return false;
		}
	}
	
	/**************** Cell ****************************/
	public static class Cell implements Comparable<Grid.Cell>{
		public final int row;
		public final int col;
		public Cell(int row, int col) {
			this.row = row;
			this.col = col;
		}
		public Cell step(Direction d) {
			return new Cell(this.row + d.row, this.col + d.col);
		}
		public int distance(Cell c) {
			return Math.abs(this.row - c.row) + Math.abs(this.col - c.col);
		}
		public int distanceSq(Cell c) {
			return (this.row - c.row) * (this.row - c.row) + (this.col - c.col) * (this.col - c.col);
		}
		public boolean isAdjacentTo(Cell c) {
			return this.distance(c) == 1;
		}
		public Direction getDirectionFrom(Cell c) {
			if (c == null) {
				return null;
			}
			if (c.row == this.row) {
				if (c.col < this.col) return Direction.EAST;
				if (c.col > this.col) return Direction.WEST;
			}
			if (c.col == this.col){
				if (c.row > this.row) return Direction.NORTH;
				if (c.row < this.row) return Direction.SOUTH;
			}
			if (c.row < this.row) {
				if (c.col < this.col) return Direction.SOUTHEAST;
				if (c.col > this.col) return Direction.SOUTHWEST;
			}
			if (c.row > this.row) {
				if (c.col < this.col) return Direction.NORTHEAST;
				if (c.col > this.col) return Direction.NORTHWEST;
			}
			return null;
		}
		@Override
		public String toString() {
			return this.getClass().getSimpleName() + "(" + row + "," + col + ")";
		}
		@Override
		public boolean equals(Object o) {
			return (o instanceof Cell) && this.row==((Cell)o).row && this.col==((Cell)o).col;
		}
		@Override
		public int hashCode() {
			return (this.row<<16) + this.col;
		}
		@Override
		public int compareTo(Cell o) {
			return this.hashCode() - o.hashCode();
		}
	}

	/**************** Direction **********************/
	public static class Direction {
		public static final Direction NORTH = new Direction(-1, 0);
		public static final Direction SOUTH = new Direction(1, 0);
		public static final Direction WEST = new Direction(0, -1);
		public static final Direction EAST = new Direction(0, 1);
		public static final Direction NORTHWEST = new Direction(-1, -1);
		public static final Direction NORTHEAST = new Direction(-1, 1);
		public static final Direction SOUTHWEST = new Direction(1, -1);
		public static final Direction SOUTHEAST = new Direction(1, 1);
		public static final Direction[] mainValues = new Direction[]{NORTH,EAST,SOUTH,WEST};
		public final int row, col;
		
		private Direction(int row, int col) {
			this.row = row;
			this.col = col;
		}
	}
	
	/**************** PathFinder **********************/
	public static interface PathFinder {
		List<Cell> findPath(Grid grid, Cell src, Cell dst);
	}
	
	/**************** Observer ************************/
	public static interface Observer {
		void update(int row, int col);
	}
	private List<Observer> observers = new ArrayList<>();
	public void addObserver(Observer ob) {
		observers.add(ob);
	}
	//special row constants for special operation
	public static final int ROW_FOR_NEW_CHESS = -1;
	public static final int ROW_FOR_RECORD = -2;
	public static final int ROW_FOR_SCORE = -3;
	public static final int ROW_FOR_GAMEOVER = -4; 
	private void alert(int row, int col) { 
		for (Observer ob : observers) {
			ob.update(row, col);
		}
	}
	
	/**************** Animation Thread *********************/
	private BlockingQueue<Cell> animationQueue = new LinkedBlockingQueue<>();
	private Thread animationThread = new Thread() {
		public void run() {
			while (true) {
				try {
					Cell current = animationQueue.take(); //block when empty
					while (current.equals(selectedCell)) {
						sleep(DELAY_MILLIS_SELECTED);
						selectAnimationFrame++;
						alert(current.row, current.col);
						if (moveToCell != null) {
							if (move(current, moveToCell)) {
								break; //out to wait for next select
							}
						}
					}
					//another cell is selected, get it from queue
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	};
	/**************** Mouse Click *********************/
	private Cell selectedCell = null;
	private int selectAnimationFrame = 0;
	public Cell getSelectedCell() { return selectedCell; }
	public int getSelectAnimationFrame() { return selectAnimationFrame; }
	private boolean moving = false;
	private Cell moveToCell = null;
	public void click(int row, int col) {
		if (moving) {
			return;
		}
		if (get(row, col) > 0) { //[select]click on a chess
			selectedCell = new Cell(row, col); //select new
			selectAnimationFrame = 0;
			animationQueue.add(selectedCell);
		} else if (selectedCell != null) { //[move]click on an empty place when a chess is selected
			moveToCell = new Cell(row, col);
		}
	}
	private boolean move(Cell from, Cell to) throws InterruptedException {
		moving = true;
		moveToCell = null;
		List<Cell> path = finder.findPath(this, from, to);
		if (path == null) {
			java.awt.Toolkit.getDefaultToolkit().beep();
		} else {
			selectedCell = null;
			Iterator<Cell> it = path.iterator();
			it.next(); //ignore the first cell, which is "from"
			while (it.hasNext()) {
				Cell next = it.next();
				step(from, next);
				from = next;
			}
			if (!checkLine(to, true)) {
				placeRandomValues();
			}
		}
		moving = false;
		return path == null ? false : true;
	}
	private void step(Cell from, Cell to) throws InterruptedException {
		Thread.sleep(DELAY_MILLIS_MOVE);
		set(to, get(from));
		set(from, 0);
	}
	
	/**************** Calculate Score ******************/
	private static final int MIN_IN_LINE = 5; //Eliminate a line when having at least 5 chess
	private boolean checkLine(Cell cell, boolean countScore) {
		int value = get(cell);
		Set<Cell> all = new TreeSet<>();
		List<Cell> line = new ArrayList<>();
		for (Cell next = cell; contains(next) && get(next) == value; next = next.step(Direction.EAST))
			line.add(next);
		for (Cell next = cell; contains(next) && get(next) == value; next = next.step(Direction.WEST))
			line.add(next);
		if (line.size() > MIN_IN_LINE) //always two "cell" in line
			all.addAll(line);

		line.clear();
		for (Cell next = cell; contains(next) && get(next) == value; next = next.step(Direction.NORTH))
			line.add(next);
		for (Cell next = cell; contains(next) && get(next) == value; next = next.step(Direction.SOUTH))
			line.add(next);
		if (line.size() > MIN_IN_LINE) //always two "cell" in line
			all.addAll(line);

		line.clear();
		for (Cell next = cell; contains(next) && get(next) == value; next = next.step(Direction.NORTHWEST))
			line.add(next);
		for (Cell next = cell; contains(next) && get(next) == value; next = next.step(Direction.SOUTHEAST))
			line.add(next);
		if (line.size() > MIN_IN_LINE) //always two "cell" in line
			all.addAll(line);

		line.clear();
		for (Cell next = cell; contains(next) && get(next) == value; next = next.step(Direction.NORTHEAST))
			line.add(next);
		for (Cell next = cell; contains(next) && get(next) == value; next = next.step(Direction.SOUTHWEST))
			line.add(next);
		if (line.size() > MIN_IN_LINE) //always two "cell" in line
			all.addAll(line);

		if (all.size() < MIN_IN_LINE) {
			return false;
		} else {
			sleep(DELAY_MILLIS_CHECKLINE);
			for (Cell e : all) { //Eliminate chess in line in TreeSet's order
				sleep(DELAY_MILLIS_CHECKLINE);
				set(e, 0);
			}
			if (countScore) {
				addScore(calculateScore(all.size()));
			}
			return true;
		}
	}
	private void sleep(int millis) {
		try {
			Thread.sleep(millis);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	//	5: 10
	//	6: 12
	//	7: 18
	//	8: 28
	//	9: 42
	//	10:82
	//TODO: havn't found the very function to calculate score as above yet
	private int calculateScore(int num) {
		return (int)(Math.pow(2, (num - MIN_IN_LINE + 1)) + 2 * (MIN_IN_LINE - 1));
	}
	private void addScore(int score) {
		this.score += score;
		alert(ROW_FOR_SCORE, this.score);
		if (this.score > this.record) { //update record when necessary
			this.record = this.score;
			alert(ROW_FOR_RECORD, this.record);
		}
	}
	
	public static final int NEW_CHESS_PER_ROUND = 3;
	private int[] nextRoundValues = new int[NEW_CHESS_PER_ROUND];
	private Random rand = new Random();

	private void generateRandomValues() {
		for (int i=0; i<nextRoundValues.length; i++) {
			nextRoundValues[i] = rand.nextInt(maxValue) + 1;
			alert(ROW_FOR_NEW_CHESS, i);
		}
	}
	private void placeRandomValues() {
		placeRandomValues(nextRoundValues.length);
	}
	private void placeRandomValues(int num) {
		assert(num <= nextRoundValues.length);
		List<Cell> room = getRoomForChess();
		for (int i=0; i<num; i++) {
			if (room.isEmpty()) {
				gameOver();
				return;
			}
			int place = rand.nextInt(room.size());
			sleep(DELAY_MILLIS_PLACE_NEW);
			set(room.get(place), nextRoundValues[i]);
			if (checkLine(room.get(place), false)) {
				room = getRoomForChess(); //some line cleared, collect room again
			} else {
				room.remove(place);
			}
		}
		if (room.isEmpty()) {
			gameOver();
			return;
		}
		generateRandomValues();
	}
	private List<Cell> getRoomForChess() {
		List<Cell> room = new ArrayList<>();
		for (int row = 0; row < getRows(); row++) {
			for (int col = 0; col < getColumns(); col++) {
				if (get(row, col) == 0) {
					room.add(new Cell(row, col));
				}
			}
		}
		return room;
	}
	private void gameOver() {
		alert(ROW_FOR_GAMEOVER, this.score);
	}
	
	private int record = 100;
	private int score = 0;
	public int getRecord() {
		return record;
	}
	public int getScore() {
		return score;
	}
	
	private final static int DELAY_MILLIS_MOVE = 50;
	private final static int DELAY_MILLIS_CHECKLINE = 50;
	private final static int DELAY_MILLIS_SELECTED = 50;
	private final static int DELAY_MILLIS_PLACE_NEW = 50;
}
